/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal class ReactiveCacheEnumerator : GroupCacheEnumerator
  {
    internal ReactiveCacheEnumerator(
      int cacheSize,
      GroupHelperBase dataHelper,
      ReactiveDataPathEnumerator reactiveDataPathEnumerator )
      : base( cacheSize, dataHelper, new SyncToAsyncEnumerator<DataPath>( reactiveDataPathEnumerator ) )
    {
      reactiveDataPathEnumerator.ShouldPerformFetchDelegate = this.CachedItems.Contains;
      reactiveDataPathEnumerator.OperationCompleted += new EventHandler<OperationCompletedEventArgs<DataPath>>( OnOperationCompleted );
    }


    #region ReactiveEnumerator Property

    private ReactiveDataPathEnumerator ReactiveEnumerator
    {
      get
      {
        return ( ReactiveDataPathEnumerator )
            ( ( SyncToAsyncEnumerator<DataPath> )
               this.ChildEnumerator ).ChildEnumerator;
      }
    }

    #endregion ReactiveEnumerator Property

    #region OperationCompleted Event

    internal event EventHandler<OperationCompletedEventArgs<DataPath>> OperationCompleted;

    private void OnOperationCompleted( object sender, OperationCompletedEventArgs<DataPath> e )
    {
      if( e.Action == OperationCompletedAction.Items )
      {
        this.OnItemsOperationCompleted( e );
      }
      else
      {
        this.OnStatResultOperationCompleted( e );
      }

      if( this.OperationCompleted != null )
      {
        this.OperationCompleted( this, e );
      }
    }

    private void OnStatResultOperationCompleted( OperationCompletedEventArgs<DataPath> e )
    {
      // Must Create a copy of the "keys" since the dictionary can be modified in the loop
      IEnumerable<StatParameters> keys = this.CachedStatResults.Keys.ToList();
      foreach( StatParameters currentKey in keys )
      {
        StatResults statResults = this.CachedStatResults[ currentKey ];

        if( object.ReferenceEquals( e.DummyStatResults, statResults ) )
        {
          Debug.Assert( object.Equals( e.StatParameters, currentKey ), "Expecting same parameters" );
          this.CachedStatResults[ currentKey ] = e.RealStatResults;
        }
      }
    }

    private void OnItemsOperationCompleted( OperationCompletedEventArgs<DataPath> e )
    {
      if( e.Replacements != null )
      {
        foreach( Tuple<DataPath, DataPath> replacement in e.Replacements )
        {
          int index = this.CachedItems.IndexOf( replacement.Item1 );
          if( index != -1 )
          {
            this.CachedItems.RemoveAt( index );
            this.CachedItems.Insert( index, replacement.Item2 );
          }
        }
      }

      if( e.Deletions != null && e.Deletions.Count > 0 )
      {
        int minIndex = this.CachedItems.Count;
        int maxIndex = -1;

        foreach( DataPath path in e.Deletions )
        {
          int pathIndex = this.CachedItems.IndexOf( path );
          if( pathIndex >= 0 )
          {
            minIndex = Math.Min( minIndex, pathIndex );
            maxIndex = Math.Max( maxIndex, pathIndex );
          }
        }

        if( e.Operation.EndPositionObject.IsBeforeFirst )
        {
          if( maxIndex >= 0 )
          {
            this.CachedItems.RemoveRange( 0, maxIndex + 1 );
            this.UpdateCountInformation( new RelativeOffset( 1, ReferencePoint.BeforeFirst ), e.Operation.TotalCount );
          }
        }
        else
        {
          Debug.Assert( e.Operation.EndPositionObject.IsAfterLast );

          if( minIndex < this.CachedItems.Count )
          {
            this.CachedItems.RemoveRange( minIndex, this.CachedItems.Count - minIndex );
            this.UpdateCountInformation( new RelativeOffset( minIndex, ReferencePoint.AfterLast ), e.Operation.TotalCount );
          }
        }
      }
      else
      {
        EstimateInt64 notNeeded;
        RelativeOffset firstItemOffset;

        this.ExtractIndexes( e.Operation, out notNeeded, out firstItemOffset );
        this.UpdateCountInformation( firstItemOffset, e.Operation.TotalCount );
      }

      //UI Specific behavior, spread values into cache.
      if( e.Replacements != null )
      {
        foreach( Tuple<DataPath, DataPath> replacement in e.Replacements )
        {
          this.SpreadNewValuesIntoCache( replacement.Item1, replacement.Item2 );
        }
      }
    }

    private void SpreadNewValuesIntoCache( DataPath oldItem, DataPath newItem )
    {
      int cacheIndex = this.CachedItems.IndexOf( newItem );

      if( cacheIndex < 0 )
        return;

      int cacheItemsCount = this.CachedItems.Count;

      Debug.Assert( newItem.Depth <= oldItem.Depth );

      DataPath newDummyTemplate = this.ReactiveEnumerator.DataItemFactory.CreateInstance( newItem );
      for( int i = cacheIndex - 1; i >= 0; i-- )
      {
        DataPath target = this.CachedItems[ i ];

        if( !this.InitializeParentFrom( target, newDummyTemplate ) )
          break;
      }

      //When the new Item is a collapsed group, New dummy sub-groups instance need to be done for other
      //direction; therefore, the newDummyTemplate is recreated.
      newDummyTemplate = this.ReactiveEnumerator.DataItemFactory.CreateInstance( newItem );
      for( int i = cacheIndex + 1; i < cacheItemsCount; i++ )
      {
        DataPath target = this.CachedItems[ i ];

        if( !this.InitializeParentFrom( target, newDummyTemplate ) )
          break;
      }
    }

    private bool InitializeParentFrom( DataPath target, DataPath source )
    {
      if( this.IsCompletelyInitialized( target ) )
        return false;

      Debug.Assert( !target[ target.Depth - 1 ].IsLoaded );

      int initCount = Math.Min( target.Depth, source.Depth ) - 1;

      for( int i = 0; i < initCount; i++ )
      {
        Debug.Assert( source[ i ].GetType() == target[ i ].GetType() );

        if( source[ i ].IsLoaded || !target[ i ].IsLoaded )
        {
          target[ i ] = source[ i ];
        }
      }
      return true;
    }

    private bool IsCompletelyInitialized( DataPath item )
    {
      //Start at the end because it is supposed to be the last
      //item initialized.
      for( int i = item.Depth - 1; i > -1; i-- )
      {
        if( !item[ i ].IsLoaded )
        {
          Debug.Assert( i == item.Depth - 1 );
          return false;
        }
      }

      return true;
    }

    #endregion OperationCompleted Event

    protected override void PathExpanded( DataPath path )
    {
      //If the collapsed path is in cache, we will flush all starting at this
      //item (ie. The collapsed path) will be flush by the base class.
      //
      // Then, Items fetched starting there will need to have this path as dummy template.

      int expandedPathIndex = this.CachedItems.IndexOf( path );
      if( expandedPathIndex != -1 )
      {
        this.SetNextItemTemplate( path );
      }
      base.PathExpanded( path );
    }

    public void SetNextItemTemplate( DataPath template )
    {
      //A "null" itemTemplate is a valid template. That
      //is why there is a bool flag
      //to hold the "validity" of the item template value.
      m_useCacheNextItemTemplate = true;
      m_cacheNextItemTemplate = template;
    }

    public void SetPreviousItemTemplate( DataPath template )
    {
      //A "null" itemTemplate is a valid template. That
      //is why there is a bool flag
      //to hold the "validity" of the item template value.
      m_useCachePreviousItemTemplate = true;
      m_cachePreviousItemTemplate = template;
    }

    protected override JumpMoveAndFetchAsyncResult BeginChildJumpMoveAndFetch(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback userCallback,
      object asyncState )
    {
      //Check if the data will be appended to the cache.
      int queryDirection = ( parameters.ExpectedMoveCount > 0 )
                             ? DataUtils.GetUnit( parameters.ExpectedMoveCount )
                             : DataUtils.GetUnit( parameters.ExpectedFetchCount );
      bool fetchCacheNext = false;
      bool fetchCachePrevious = false;

      if( this.CachedItems.Count == 0 )
      {
        fetchCacheNext = ( queryDirection > 0 );
        fetchCachePrevious = ( queryDirection < 0 );
      }
      else
      {
        Nullable<int> sourcePositionIndex = this.FindSourcePositionIndex(
          parameters.SourcePosition,
          parameters.ExpectedMoveCount,
          parameters.ExpectedFetchCount );

        if( sourcePositionIndex.HasValue )
        {
          long fetchStartIndex = sourcePositionIndex.Value + parameters.ExpectedMoveCount;
          long fetchEndIndex = fetchStartIndex + ( parameters.ExpectedFetchCount - DataUtils.GetUnit( parameters.ExpectedFetchCount ) );

          if( fetchStartIndex >= 0 && fetchStartIndex < this.CachedItems.Count )
            throw new DataInternalException();//Not expecting a child fetch starting within the bound of the cache.

          fetchCacheNext = ( fetchStartIndex <= this.CachedItems.Count ) && ( fetchEndIndex >= this.CachedItems.Count )
                        || ( fetchStartIndex >= this.CachedItems.Count ) && ( fetchEndIndex <= this.CachedItems.Count );

          fetchCachePrevious = ( fetchStartIndex <= 0 ) && ( fetchEndIndex >= 0 )
                            || ( fetchStartIndex >= 0 ) && ( fetchEndIndex <= 0 );
        }
      }

      //In theses case we consider the m_cacheNextItemTemplate and m_cachePreviousItemTemplate values
      if( fetchCacheNext && m_useCacheNextItemTemplate )
      {
        Debug.Assert( !fetchCachePrevious );

        //Cache will be append this data to the end of cache.
        this.ReactiveEnumerator.SetItemTemplateOverride( m_cacheNextItemTemplate );
      }
      else if( fetchCachePrevious && m_useCachePreviousItemTemplate )
      {
        Debug.Assert( !fetchCacheNext );

        //Cache will be append this data to the begin of cache.
        this.ReactiveEnumerator.SetItemTemplateOverride( m_cachePreviousItemTemplate );
      }

      JumpMoveAndFetchAsyncResult asyncResult = base.BeginChildJumpMoveAndFetch(
                                                  parameters,
                                                  userCallback,
                                                  asyncState );

      //The ItemOverride can now be cleared
      m_useCacheNextItemTemplate = false;
      m_useCachePreviousItemTemplate = false;
      m_cacheNextItemTemplate = null;
      m_cachePreviousItemTemplate = null;

      this.ReactiveEnumerator.ClearItemTemplateOverride();

      return asyncResult;
    }

    protected override Nullable<int> FindSourcePositionIndex(
      SourcePosition sourcePosition,
      long expectedMoveCount,
      int expectedFetchCount )
    {
      if( sourcePosition is DummyPosition<DataPath> )
      {
        DataPath item;
        long offset;
        SourcePosition position;
        bool approximateOffset;
        DummyPosition<DataPath> dummyPosition = ( DummyPosition<DataPath> )sourcePosition;
        Func<DataPath, SourcePosition> positionResolver;

        if( DataUtils.ResolvePosition( dummyPosition, false, out position, out item, out offset, out approximateOffset, out positionResolver ) )
        {
          int index = this.CachedItems.IndexOf( item );

          return ( index != -1 )
                   ? Convert.ToInt32( index + offset )
                   : ( Nullable<int> )null;
        }
        else
        {
          Nullable<int> positionIndex = base.FindSourcePositionIndex( position, expectedMoveCount, expectedFetchCount );

          return ( positionIndex.HasValue )
                   ? Convert.ToInt32( positionIndex.Value + offset )
                   : ( Nullable<int> )null;
        }
      }
      else if( sourcePosition is DependentPosition<DataPath> )
      {
        Debug.Assert( ( ( DependentPosition<DataPath> )sourcePosition ).PositionResolver != null,
          "If this is raised, a optimisation could be done by seaching the cache for the item." );

        return null;
      }

      return base.FindSourcePositionIndex( sourcePosition, expectedMoveCount, expectedFetchCount );
    }

    protected override void Dispose( bool disposing )
    {
      if( disposing )
      {
        if( this.ReactiveEnumerator.ShouldPerformFetchDelegate == this.CachedItems.Contains )
        {
          this.ReactiveEnumerator.ShouldPerformFetchDelegate = null; 
        }
        this.ReactiveEnumerator.OperationCompleted -= new EventHandler<OperationCompletedEventArgs<DataPath>>( OnOperationCompleted );
      }

      base.Dispose( disposing );
    }

    #region Private Fields

    private DataPath m_cacheNextItemTemplate;
    private DataPath m_cachePreviousItemTemplate;
    private bool m_useCacheNextItemTemplate;
    private bool m_useCachePreviousItemTemplate;

    #endregion Private Fields
  }
}
